Gate Every Prose Rule Now That the Backlog Is Zero - #620
Conversation
Closes the last piece of #519. The whole-tree count reached zero at `20916ad`, so `semicolon` and `dash` no longer need the warn-only tier they were given while a backlog existed. Both now block, alongside the five rules that already did. `home-path` joins them, and that is the substantive fix rather than a tidy-up. It has been in `DEFAULT_RULES` since it was written, yet was named by neither CI step, so the pattern-detectable half of the representative-data rule ran on a bare local invocation and gated nothing in CI. `OPERATIONS.md` recorded that hole, and this closes it and drops the record. `charset-unknown` stays warn-only, deliberately. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that typed it can fix. The `prose-gate` composite action is unaffected. It runs `--diff` with no `--check` list, so a downstream caller already gates the full default set over the lines a change touches. Verification, all on this branch: the new gating invocation exits 0 whole-tree, `charset-unknown` reports 0 findings, `test_prose_lint.py` passes 198 tests, `repo_gate.py` is clean, `actionlint` exits 0, `editorconfig-checker` exits 0, and `spec/validate.py` reports OK. Also removes the #519 entry from `TODO.md`, now that the issue is closed with its evidence quoted. Three figures in that entry were wrong and are corrected in the closing comment rather than carried forward: a commit id that resolves to nothing, a batch total that mixed checkers, and a 37-of-38 attribution that is 38 of 38 when both checkers run over the same bytes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the hub’s CI and documentation so that, now the prose backlog is cleared, the whole-tree validation workflow gates on all default prose_lint.py rules (including home-path) except charset-unknown, which remains warn-only.
Changes:
- Expand the validate workflow’s prose gate invocation to include
semicolon,dash, andhome-path, and narrow the warn-only step tocharset-unknownonly. - Update
OPERATIONS.mdandscripts/README.mdto reflect the new CI invocation and rationale for the remaining warn-only rule. - Remove the completed #519 entry and link definition from
TODO.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| TODO.md | Removes the completed #519 tracking entry and its reference link definition. |
| scripts/README.md | Updates prose_lint documentation to reflect the “all rules gate except charset-unknown” posture and adds home-path context. |
| OPERATIONS.md | Updates the “run gates like CI” runbook commands and explanations to match the new gating/warn-only split. |
| .github/workflows/validate-task.yml | Changes CI to gate additional prose rules whole-tree and renames/repurposes the warn-only step for charset-unknown. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot found `scripts/README.md` contradicting itself: the section head now says every default rule gates, while its closing paragraph still listed `semicolon` and `dash` as warn-only with a backlog to sweep. Rewritten to state what the tree-wide zero buys, and to point at where the `charset-unknown` exception is already explained rather than restating it. Swept by term rather than by instance, which found a second hit the finding did not name: `test_prose_lint.py` calls the warn-only step the consumer of the `--diff` scope. It never was, since both hub steps read the whole tree and the diff-scoped consumer is the `prose-gate` composite action. That is pre-existing rather than introduced here, and it is corrected in the same pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
OPERATIONS.md:11
- This says "
sentence-splitis in neither and is asked for by name", but a later paragraph in this section sayssentence-splitis "named by no invocation, so nothing runs it". To avoid contradicting the runbook's own gap list, reword this sentence to clarify thatsentence-splitis not invoked by CI and must be explicitly requested to run.
CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists together. What differs is the exit code rather than the coverage: CI gates on eight of the nine and reports `charset-unknown` warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither and is asked for by name. Run the CI invocations:
.github/workflows/validate-task.yml:72
- This comment says "Every prose rule" is clean and gating, but
scripts/prose_lint.pyimplementssentence-splitand the runbooks note it is not invoked by CI. Consider narrowing the wording to "Every default prose rule" so it matches what this workflow actually runs.
# Every prose rule is clean tree-wide, so each one gates rather than reporting a backlog.
# `charset-unknown` is clean too and still reports, for the reason the step below gives.
Round two raised two suppressed findings, both the same shape and both correct. The workflow comment claimed every prose rule is clean and gating, which overstates it: `sentence-split` is a prose rule, is clean tree-wide, and gates nowhere, because it sits outside `DEFAULT_RULES` on purpose. `OPERATIONS.md` said `sentence-split` is asked for by name a few lines above saying nothing runs it. Both readings are true and they read as a contradiction, so the first now says nothing below runs it and a local run reaches it only by naming it. Sweeping the phrasing rather than the two named lines found a third instance in `scripts/README.md`, where every rule above carried the same overstatement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2, both suppressed findings accepted and fixed in
|
| Check | Result |
|---|---|
| Gating invocation, whole tree | 0 violations across 0 files |
sentence-split, whole tree |
0 violations across 0 files |
python3 scripts/test_prose_lint.py |
198 tests, OK |
python3 scripts/repo_gate.py |
eol 0, sha-pin 0 |
actionlint |
exit 0 |
editorconfig-checker |
exit 0 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
scripts/README.md:21
- The new
home-pathdescription does not mention thatprose_lint.pyskips this check when run from an operational repository checkout. Without that caveat, the README implies the rule always runs and may mislead operators reviewing why a local run didn’t flag an absolute path.
Enforces the [`GOVERNANCE.md`][governance] "Documentation Style Conventions" rules that no linter checks: non-ASCII judged against the charset rule's three tiers, a semicolon in prose, a spaced hyphen joining or interrupting a sentence, a duplicated consecutive word, a British spelling, and the shape of a comment's prose. It carries one rule from elsewhere in that document, `home-path`, which comes from "Representative Data in Agent-Authored Text" and catches an absolute home path naming a real account. That rule closes the pattern-detectable sliver of its section and nothing beyond it, since the exposure the section exists for was name-shaped and no pattern finds a name.
Round three raised one suppressed finding, and it is correct. The new `home-path` paragraph read as though the rule always runs, where `prose_lint.py` drops it in an operational repository, whose runbook carries the literal path an operator types rather than data an agent observed. Verified in the code rather than taken on trust. The skip is real and announced on stderr, and it keys on which repository-configuration payload the checkout carries. The hub carries both, being the template for each, so `operational_checkout` reads False here and the rule does gate on this repository, which is what makes the CI promotion in this pull request meaningful rather than a rule that quietly excuses itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3, the suppressed finding accepted and fixed in
|
| Check | Result |
|---|---|
| Gating invocation, whole tree | 0 violations across 0 files |
python3 scripts/test_prose_lint.py |
198 tests, OK |
python3 scripts/repo_gate.py |
eol 0, sha-pin 0 |
editorconfig-checker |
exit 0 |
Round 4 is clean, and the loop is closedCopilot reviewed 5 of 5 files on
Two of the four rounds reported no comments while carrying findings in a A sweep beat the review to a third instance twice. Rounds 1 and 2 each named one line, and grepping the term rather than fixing the named line found another instance the review had not reached, in Final verification on
|
| Check | Result |
|---|---|
| Gating invocation, whole tree | 0 violations across 0 files |
charset-unknown, whole tree |
0 violations across 0 files |
sentence-split, whole tree |
0 violations across 0 files |
python3 scripts/test_prose_lint.py |
198 tests, OK |
python3 scripts/repo_gate.py |
eol 0, sha-pin 0 |
actionlint |
exit 0 |
editorconfig-checker |
exit 0 |
python3 spec/validate.py |
22 cataloged, 0 backlog repos classify cleanly |
| CI on the head | Validate sources job and workflow status job both SUCCESS |
One review thread stays unresolved rather than declined. The reply carrying the fix is on it, and resolving it is the maintainer's action here.
Adds a work cluster for the two failures this pull request's own review loop hit at its last step, which the runbook has no shape for. The resolve mutation was refused by the agent harness's permission layer before any request left the machine, seconds after a reply carrying the identical thread id had succeeded and returned a comment url. The runbook's dead-path list is entirely GitHub's own refusals, so a local refusal matches none of them and reads as a bad identifier, which invites the one response a blocked write must never get. Handing the resolve over then failed a second time. `Q_THREADS` selects a thread's id, resolved state, path and first comment, and not its `url`, so the digest names a thread by a `PRRT_` node id that appears nowhere in the GitHub interface. Verified against the query rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 5 is clean, and the merge state is
|
| Bullet | Finding |
|---|---|
| Document it | The runbook's dead-path list holds only GitHub's own refusals, so a refusal raised locally matches none of them and reads as a bad identifier. |
| Detect it | reply already exits 63 where the resolve did not report the thread resolved. A loop driving gh api by hand gets no exit code, so the state is the evidence rather than the mutation returning. |
| Resolve it | Q_THREADS selects a thread's id, resolved state, path and first comment, and not its url, so the digest names a thread it cannot point at. |
Implementing the url selection is left to that cluster rather than done here, since it changes scripts/pr_review.py and this pull request is about the prose gate.
Verification on 6fa593b
| Check | Result |
|---|---|
| Gating invocation, whole tree | 0 violations across 0 files |
charset-unknown and sentence-split, whole tree |
0 violations across 0 files |
python3 scripts/test_prose_lint.py |
198 tests, OK |
python3 scripts/repo_gate.py |
eol 0, sha-pin 0 |
python3 spec/validate.py |
22 cataloged, 0 backlog repos classify cleanly |
editorconfig-checker |
exit 0 |
markdownlint-cli2 on TODO.md |
0 issues |
Fixes a defect the [#621](#621) promotion's review found. A promotion's head is `develop`, so the fix cannot land on that pull request and takes its own branch, which the promotion then carries. ## The defect The gating step's comment claimed every default prose rule gates, one line above the comment naming `charset-unknown` as the one that reports instead. Both cannot be true, and the second is. ```text # Every default prose rule is clean tree-wide, so each one gates rather than reporting a backlog. # `charset-unknown` is clean too and still reports, for the reason the step below gives. ``` ## Where the overstatement came from It is the previous fix's. Round 2 of [#620](#620) narrowed "every prose rule" to "every default prose rule", to stop the claim covering `sentence-split`, which sits outside `DEFAULT_RULES` and gates nowhere. That narrowing walked straight into `charset-unknown`, which sits inside `DEFAULT_RULES` and still does not gate. | Rule | In `DEFAULT_RULES` | Gates in CI | | --- | --- | --- | | `charset`, `semicolon`, `dash`, `dupword`, `spelling`, `comment-wrap`, `comment-case`, `home-path` | yes | yes | | `charset-unknown` | yes | no, warn-only by design | | `sentence-split` | no | no | One phrase cannot carry both exclusions. The line now states the count and lets the next line name which rule it is. ## Swept rather than patched at the reported line The same shape was in `scripts/README.md`, where one sentence said being clean is what lets CI gate them and the next sentence excepted one of them. That instance was not reported and is corrected here, since a claim written in two places drifts in two places. ## Verification | Check | Result | | --- | --- | | Gating invocation, whole tree | 0 violations across 0 files | | `python3 scripts/test_prose_lint.py` | 198 tests, OK | | `actionlint` | exit 0 | | `editorconfig-checker` | exit 0 | --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ule (#621) Promotes three changes, 7 files. The second and third exist because this promotion's own review found defects in the first, and a promotion's head is `develop`, so each took its own branch and its own review before this could carry it. | Commit | Pull request | What it does | | --- | --- | --- | | `26af061` | [#620](#620) | Gates every default prose rule whole-tree, closing [#519](#519) | | `039be28` | [#622](#622) | Names the one default rule that does not gate, found by this promotion's review | | `674a27a` | [#624](#624) | Carries the permission rules the review loop needs in the host installer | ## What lands on `main` **The prose gate now blocks.** The backlog reached zero at `20916ad`, so the warn-only tier that `semicolon` and `dash` were given while several hundred findings sat in the tree has expired. Both block now, and `home-path` joins them. `home-path` is the substantive half. It had been in `DEFAULT_RULES` since it was written, so every bare local run included it, and it was named by neither CI step. The rule that catches an absolute home path naming a real account therefore ran on a developer's machine and gated nothing in CI, which is the pattern-detectable sliver of [`GOVERNANCE.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/GOVERNANCE.md) "Representative Data in Agent-Authored Text". [`OPERATIONS.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/OPERATIONS.md) had recorded the hole, and this closes it and drops the record. `charset-unknown` stays warn-only, deliberately. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that typed it can fix. **The agent-safety installer carries permission rules.** It configured `settings.json` for the hook alone, so a rule added on one machine reached none of the others. Each rule is declared as a prefix and a rule, ownership requires a delimiter after the prefix so a hand-written rule for a different script is never claimed, and a re-run updates a changed rule in place. The list holds one rule today, for `scripts/pr_review.py`. The installer also refuses a malformed `settings.json` at every level it descends rather than raising a traceback partway through its edits. ## What is unchanged The [`prose-gate`](https://github.com/ptr727/ProjectTemplate/blob/develop/.github/actions/prose-gate/action.yml) composite action runs `--diff` with no `--check` list, so it already gated the full default set over the lines a change touches. A downstream caller sees no behavior change from this promotion. Only the hub's own whole-tree steps move. Nothing is installed on any machine by this. Running the installer per host stays the maintainer's action, tracked by [#365](#365). ## One property of the gate worth stating on `main` `validate-task.yml` is `workflow_call`, invoked by `test-pull-request.yml` on `pull_request` to `main` and `develop`. There is no push trigger, so a merge commit never runs it. The promoted invocation was exercised by this pull request's own CI run on `26af061`, where `Check prose step` and `Report unclassified characters step` both appear and both pass. ## Review record Twenty-one Copilot rounds across the three pull requests, 5 on #620, 2 on #622, and 12 on #624, plus 2 here. Several rounds reported no comments while carrying findings in a `<details>` block, which is the shape a thread-polling loop reads as a pass. Worth recording rather than hidden in the count: nearly every finding was in a claim the change made about itself rather than in what the code does. A count that included the rule it was counting against, a scope claim an absolute path would not have fixed, a "read once" that read twice, and a "nothing was written" printed after the hook was already on disk. ## Verification Run on `develop` at `674a27a`: | Check | Result | | --- | --- | | Gating invocation, whole tree | 0 violations across 0 files | | `charset-unknown` and `sentence-split`, whole tree | 0 violations across 0 files | | `python3 scripts/test_prose_lint.py` | 198 tests, OK | | `python3 scripts/repo_gate.py` | `eol` 0, `sha-pin` 0 | | `python3 host-setup/agent-safety/gh-write-guard.py --selftest` | PASS | | `python3 spec/validate.py` | 22 cataloged, 0 backlog repos classify cleanly | | `editorconfig-checker` | exit 0 | | Installer, fresh `CLAUDE_HOME` | hook registered, permission rule added | Merge with a merge commit rather than a squash, and without `--delete-branch`, since this pull request's head is `develop`.
Closes the last piece of #519, which is now closed with its evidence quoted on the issue.
Why now
The prose backlog reached zero at
20916ad.semicolonanddashwere warn-only because a whole-tree gate on them would have failed every run while several hundred findings sat in the tree. That reason has expired, and a warn-only tier over a clean tree is how the backlog grows back.Measured on this branch before writing the change, and again after:
home-pathgated nothing anywhere, which is the substantive fixhome-pathhas been inDEFAULT_RULESsince it was written, so a bare local run has always included it. It was named by neither CI step, so the rule that catches an absolute home path naming a real account ran on a developer's machine and gated nothing in CI. That is the pattern-detectable sliver ofGOVERNANCE.md"Representative Data in Agent-Authored Text", the section that exists because real paths carrying real names reached a public comment.OPERATIONS.mdalready recorded the hole, correctly and in detail. This closes it and drops the record, rather than leaving a runbook describing a gap that no longer exists.What stays warn-only, and why that is not an oversight
charset-unknownalone. A finding there names a character no tier covers, and classifying one is a fleet-law edit rather than something the change that happened to type it can fix. Blocking on it would make an un-tiered character an unmergeable change instead of a question for the maintainer. The step keepscontinue-on-error: trueand is renamed from "Report prose backlog step" to "Report unclassified characters step", since there is no backlog left for it to report.sentence-splitremains named by no invocation and stays that way here. It is deliberately outsideDEFAULT_RULES, so promoting it is a separate decision rather than a consequence of this one, andOPERATIONS.mdstill records it as a gap.The composite action is unaffected
.github/actions/prose-gaterunspython3 "$SCRIPT" --diff "$BASE"with no--checklist, so it already gates the full default set,home-pathincluded, over the lines a change touches. A downstream caller sees no behavior change from this pull request. Only the hub's own whole-tree steps move.Documentation kept level with the change
OPERATIONS.mdcarries the two CI invocations verbatim so a local run matches CI rather than exceeding it, so both lines are updated, the three-gap paragraph becomes two, and the sentence describing the second invocation as a backlog report is corrected.scripts/README.mdclaimed five rules gate and the rest report. It now states the tree is zero and every default rule gates but one. It also nameshome-pathfor the first time, including the limit that it closes a sliver of its section and nothing more, because the exposure that section exists for was name-shaped and no pattern finds a name.TODO.mdloses the prose_lint: the hub's own docs, spec, and catalog do not pass the gate it ships #519 entry and its link definition.Three corrections the closing comment carries rather than this diff
The
TODO.mdentry being removed held three wrong figures, each re-measured before the issue was closed:f7a6a13(snippets)b002fac(#600).69688ec.Verification
charset-unknown, whole treepython3 scripts/test_prose_lint.pypython3 scripts/repo_gate.pyeol0,sha-pin0actionlinteditorconfig-checkerpython3 spec/validate.pyThe four edited files keep their declared line endings, CRLF for the three Markdown files and LF for the workflow, which
editorconfig-checkerconfirms.